Crate signifix[][src]

Number Formatter of Fixed Significance with Metric or Binary Prefix

Formats a given number in one of the three Signifix notations as defined below by determining

  1. the appropriate metric or binary prefix and
  2. the decimal mark position in such a way as to sustain a fixed number of four significant figures.

Contents

Signifix Notations

Three notations are defined,

With Metric Prefix

The two Signifix notations with metric prefix comprise

  • a signed significand of four significant figures normalized from ±1.000 to ±999.9 to cover the three powers of ten of a particular metric prefix with the three different decimal mark positions between these four figures, and
  • a metric prefix symbol or its placeholder in case of no prefix
    • either being appended along with a whitespace as in ±1.234␣k, that is the default notation,
    • or replacing the decimal mark of the significand as in ±1k234, that is the alternate notation.

In default notation the placeholder is another whitespace as in ±1.234␣␣ to align consistently, while in alternate notation it is a number sign as in ±1#234 to conspicuously separate the integer from the fractional part of the significand. The locale-sensitive decimal mark defaults to a decimal point. The plus sign of positive numbers is optional.

With Binary Prefix

The one Signifix notation with binary prefix comprises

  • a signed significand of four significant figures normalized from ±1.000 over ±999.9 to ±1 023 to cover the four powers of ten of a particular binary prefix with the three different decimal mark positions between these four figures and a thousands separator, and
  • a binary prefix symbol or its placeholder in case of no prefix being appended along with a whitespace as in ±1.234␣Ki.

To align consistently, the placeholder is another two whitespaces as in ±1.234␣␣␣. The locale-sensitive decimal mark defaults to a decimal point while the locale-sensitive thousands separator defaults to a whitespace as in ±1␣023␣Ki. The plus sign of positive numbers is optional.

Usage

This crate is on crates.io and can be used by adding signifix to the dependencies in your project's Cargo.toml:

[dependencies]
signifix = "0.9"

# Optionally enable `try_from` support on nightly Rust.
#[dependencies.signifix]
#features = ["nightly"]

and this to your crate root:

// Optionally enable `try_from` support on nightly Rust.
// Required if the `nightly` feature is enabled in your `Cargo.toml`.
//#![feature(try_from)]

extern crate signifix;

Examples

The Notations

The Signifix notations result in a fixed number of characters preventing jumps to the left or right while making maximum use of their occupied space:

use signifix::TryFrom; // Until stabilized.

use signifix::{metric, binary, Result};

let metric = |number| -> Result<(String, String)> {
    let number = metric::Signifix::try_from(number)?;
    Ok((format!("{}", number), format!("{:#}", number)))
};
let binary = |number| -> Result<String> {
    let number = binary::Signifix::try_from(number)?;
    Ok(format!("{}", number))
};

// Three different decimal mark positions covering the three powers of ten
// of a particular metric prefix.
assert_eq!(metric(1E-04), Ok(("100.0 µ".into(), "100µ0".into()))); // 3rd
assert_eq!(metric(1E-03), Ok(("1.000 m".into(), "1m000".into()))); // 1st
assert_eq!(metric(1E-02), Ok(("10.00 m".into(), "10m00".into()))); // 2nd
assert_eq!(metric(1E-01), Ok(("100.0 m".into(), "100m0".into()))); // 3rd
assert_eq!(metric(1E+00), Ok(("1.000  ".into(), "1#000".into()))); // 1st
assert_eq!(metric(1E+01), Ok(("10.00  ".into(), "10#00".into()))); // 2nd
assert_eq!(metric(1E+02), Ok(("100.0  ".into(), "100#0".into()))); // 3rd
assert_eq!(metric(1E+03), Ok(("1.000 k".into(), "1k000".into()))); // 1st
assert_eq!(metric(1E+04), Ok(("10.00 k".into(), "10k00".into()))); // 2nd
assert_eq!(metric(1E+05), Ok(("100.0 k".into(), "100k0".into()))); // 3rd
assert_eq!(metric(1E+06), Ok(("1.000 M".into(), "1M000".into()))); // 1st

// Three different decimal mark positions and a thousands separator covering
// the four powers of ten of a particular binary prefix.
assert_eq!(binary(1_024f64.powi(0) * 1E+00), Ok("1.000   ".into())); // 1st
assert_eq!(binary(1_024f64.powi(0) * 1E+01), Ok("10.00   ".into())); // 2nd
assert_eq!(binary(1_024f64.powi(0) * 1E+02), Ok("100.0   ".into())); // 3rd
assert_eq!(binary(1_024f64.powi(0) * 1E+03), Ok("1 000   ".into())); // 4th
assert_eq!(binary(1_024f64.powi(1) * 1E+00), Ok("1.000 Ki".into())); // 1st
assert_eq!(binary(1_024f64.powi(1) * 1E+01), Ok("10.00 Ki".into())); // 2nd
assert_eq!(binary(1_024f64.powi(1) * 1E+02), Ok("100.0 Ki".into())); // 3rd
assert_eq!(binary(1_024f64.powi(1) * 1E+03), Ok("1 000 Ki".into())); // 4th
assert_eq!(binary(1_024f64.powi(2) * 1E+00), Ok("1.000 Mi".into())); // 1st

// Rounding over prefixes is safe against floating-point inaccuracies.
assert_eq!(metric(999.949_999_999_999_8),
    Ok(("999.9  ".into(), "999#9".into())));
assert_eq!(metric(999.949_999_999_999_9),
    Ok(("1.000 k".into(), "1k000".into())));
assert_eq!(binary(1_023.499_999_999_999_94),
    Ok("1 023   ".into()));
assert_eq!(binary(1_023.499_999_999_999_95),
    Ok("1.000 Ki".into()));

Transfer Rate

This is useful to smoothly refresh a transfer rate within a terminal:

use signifix::TryFrom; // Until stabilized.

use std::f64;
use std::time::Duration;
use signifix::metric::{Signifix, Error, DEF_MIN_LEN};

let transfer_rate = |bytes: u64, duration: Duration| -> String {
    let seconds = duration.as_secs() as f64
        + duration.subsec_nanos() as f64 * 1E-09;
    let bytes_per_second = bytes as f64 / seconds;
    let unit = "B/s";
    let rate = match Signifix::try_from(bytes_per_second) {
        Ok(rate) => if rate.factor() < 1E+00 {
            " - slow - ".into() // instead of mB/s, µB/s, ...
        } else {
            format!("{}{}", rate, unit) // normal rate
        },
        Err(case) => match case {
            Error::OutOfLowerBound(rate) => if rate == 0f64 {
                " - idle - " // no progress at all
            } else {
                " - slow - " // almost no progress
            },
            Error::OutOfUpperBound(rate) => if rate == f64::INFINITY {
                " - ---- - " // zero nanoseconds
            } else {
                " - fast - " // awkwardly fast
            },
            Error::Nan => " - ---- - ", // zero bytes in zero nanoseconds
        }.into(),
    };
    debug_assert_eq!(rate.chars().count(),
        DEF_MIN_LEN + unit.chars().count());
    rate
};

assert_eq!(transfer_rate(42_667, Duration::from_secs(300)), "142.2  B/s");
assert_eq!(transfer_rate(42_667, Duration::from_secs(030)), "1.422 kB/s");
assert_eq!(transfer_rate(42_667, Duration::from_secs(003)), "14.22 kB/s");
assert_eq!(transfer_rate(00_001, Duration::from_secs(003)), " - slow - ");
assert_eq!(transfer_rate(00_000, Duration::from_secs(003)), " - idle - ");
assert_eq!(transfer_rate(42_667, Duration::from_secs(000)), " - ---- - ");

Measured Amps

Or to monitor a measured quantity like an electrical current including its direction with positive numbers being padded to align with negative ones:

use signifix::TryFrom; // Until stabilized.

use signifix::metric::{Signifix, Result, DEF_MAX_LEN};

let measured_amps = |amps| -> Result<String> {
    if let Some(amps) = amps {
        Signifix::try_from(amps)
            .map(|amps| format!("{:>1$}A", amps, DEF_MAX_LEN))
    } else {
        Ok("     0  A".into())
    }
};

assert_eq!(measured_amps(Some( 1.476E-06)), Ok(" 1.476 µA".into()));
assert_eq!(measured_amps(None),             Ok("     0  A".into()));
assert_eq!(measured_amps(Some(-2.927E-06)), Ok("-2.927 µA".into()));

Filesize Diff

While to visualize a change in file size, a plus sign might be preferred for positive numbers:

use signifix::TryFrom; // Until stabilized.

use signifix::metric::{Signifix, Error, Result};

let filesize_diff = |curr, prev| -> Result<String> {
    Signifix::try_from(curr - prev).map(|diff| format!("{:+#}", diff))
        .or_else(|case| if case == Error::OutOfLowerBound(0f64)
            { Ok("=const".into()) } else { Err(case) })
};

assert_eq!(filesize_diff(78_346, 57_393), Ok("+20k95".into()));
assert_eq!(filesize_diff(93_837, 93_837), Ok("=const".into()));
assert_eq!(filesize_diff(27_473, 36_839), Ok("-9k366".into()));

Boundary Stat

The binary prefix instead suits well to visualize quantities being multiples of powers of two, such as memory boundaries due to binary addressing:

use signifix::TryFrom; // Until stabilized.

use signifix::binary::{Signifix, Error, Result};

let boundary_stat = |used: u64, size: u64| -> Result<String> {
    if used == 0 {
        let size = Signifix::try_from(size)?;
        return Ok(format!("    0   B (    0 %) of {}B", size));
    }
    let p100 = Signifix::try_from(used as f64 / size as f64 * 100.0)
        .map(|p100| format!("{:.*} %", p100.exponent(), p100.significand()))
        .or_else(|error| if let Error::OutOfLowerBound(_) = error
            { Ok("  < 1 %".into()) } else { Err(error) })?;
    let used = Signifix::try_from(used)?;
    let size = Signifix::try_from(size)?;
    Ok(format!("{}B ({}) of {}B", used, p100, size))
};

assert_eq!(boundary_stat(0_000u64.pow(1), 1_024u64.pow(3)),
    Ok("    0   B (    0 %) of 1.000 GiB".into()));
assert_eq!(boundary_stat(1_024u64.pow(2), 1_024u64.pow(3)),
    Ok("1.000 MiB (  < 1 %) of 1.000 GiB".into()));
assert_eq!(boundary_stat(3_292u64.pow(2), 1_024u64.pow(3)),
    Ok("10.34 MiB (1.009 %) of 1.000 GiB".into()));
assert_eq!(boundary_stat(8_192u64.pow(2), 1_024u64.pow(3)),
    Ok("64.00 MiB (6.250 %) of 1.000 GiB".into()));
assert_eq!(boundary_stat(1_000u64.pow(3), 1_024u64.pow(3)),
    Ok("953.7 MiB (93.13 %) of 1.000 GiB".into()));
assert_eq!(boundary_stat(1_024u64.pow(3), 1_024u64.pow(3)),
    Ok("1.000 GiB (100.0 %) of 1.000 GiB".into()));

Localizations

Until there is a recommended and possibly implicit localization system for Rust, explicit localization can be achieved by wrapping the Signifix type into a locale-sensitive newtype which implements the Display trait via the Signifix::fmt() method:

use signifix::TryFrom; // Until stabilized.

use signifix::binary::{Signifix, Result};

struct SignifixSi(Signifix); // English SI style (default)
struct SignifixEn(Signifix); // English locale (whitespace -> comma)
struct SignifixDe(Signifix); // German locale (comma <-> point)

impl std::fmt::Display for SignifixSi {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        std::fmt::Display::fmt(&self.0, f)
    }
}
impl std::fmt::Display for SignifixEn {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        self.0.fmt(f, ".", ",")
    }
}
impl std::fmt::Display for SignifixDe {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        self.0.fmt(f, ",", ".")
    }
}

let localizations = |number| -> Result<(String, String, String)> {
    Signifix::try_from(number).map(|number| (
        format!("{}", SignifixSi(number)),
        format!("{}", SignifixEn(number)),
        format!("{}", SignifixDe(number)),
    ))
};

assert_eq!(localizations(999.9f64 * 1_024f64),
    Ok(("999.9 Ki".into(), "999.9 Ki".into(), "999,9 Ki".into())));
assert_eq!(localizations(1_000f64 * 1_024f64),
    Ok(("1 000 Ki".into(), "1,000 Ki".into(), "1.000 Ki".into())));

Customization

Customization can be achieved by extracting information from the Signifix type via its methods:

use signifix::TryFrom; // Until stabilized.

use signifix::metric::{Signifix, Result};

struct SignifixTable<'a>(&'a[Signifix]);

impl<'a> std::fmt::Display for SignifixTable<'a> {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        f.pad(" Int Fra   10³\n")?;
        f.pad("---- ---- ----\n")?;
        for entry in self.0 {
            let (integer, fractional) = entry.parts();
            f.pad(&format!("{:4} {:<3}    {:2}\n",
                integer, fractional, entry.prefix() as i32 - 8))?;
        }
        Ok(())
    }
}

let customization = |entries: &[_]| -> Result<String> {
    let mut table = Vec::with_capacity(entries.len());
    for entry in entries {
        table.push(Signifix::try_from(*entry)?);
    }
    Ok(SignifixTable(&table).to_string())
};

assert_eq!(customization(&[
     1.234E-06,
     12.34E+00,
    -123.4E+24,
]), Ok(concat!(
    " Int Fra   10³\n",
    "---- ---- ----\n",
    "   1 234    -2\n",
    "  12 34      0\n",
    "-123 4       8\n",
).into()));

Modules

binary

Formatter of Signifix default notation with binary prefix.

metric

Formatter of Signifix default and alternate notation with metric prefix.

Enums

Error

A common error arising from this crate's modules.

Traits

TryFrom

Required until the try_from feature stabilised.

TryInto

Required until the try_from feature stabilised.

Type Definitions

Result

The canonical Result type using this crate's Error type.